from datetime import datetime, timedelta
from random import randint
from unittest.mock import MagicMock, PropertyMock

import ccxt
import pandas as pd
import pytest

from freqtrade.enums import CandleType, MarginMode, TradingMode
from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException
from freqtrade.exchange.exchange_utils_timeframe import timeframe_to_seconds
from freqtrade.persistence import Trade
from freqtrade.util.datetime_helpers import dt_from_ts, dt_ts, dt_utc
from tests.conftest import EXMS, get_patched_exchange
from tests.exchange.test_exchange import ccxt_exceptionhandlers


@pytest.mark.parametrize(
    "side,order_type,time_in_force,expected",
    [
        ("buy", "limit", "gtc", {"timeInForce": "GTC"}),
        ("buy", "limit", "IOC", {"timeInForce": "IOC"}),
        ("buy", "market", "IOC", {}),
        ("buy", "limit", "PO", {"timeInForce": "PO"}),
        ("sell", "limit", "PO", {"timeInForce": "PO"}),
        ("sell", "market", "PO", {}),
    ],
)
def test__get_params_binance(default_conf, mocker, side, order_type, time_in_force, expected):
    exchange = get_patched_exchange(mocker, default_conf, exchange="binance")
    assert exchange._get_params(side, order_type, 1, False, time_in_force) == expected


@pytest.mark.parametrize("trademode", [TradingMode.FUTURES, TradingMode.SPOT])
@pytest.mark.parametrize(
    "limitratio,expected,side",
    [
        (None, 220 * 0.99, "sell"),
        (0.99, 220 * 0.99, "sell"),
        (0.98, 220 * 0.98, "sell"),
        (None, 220 * 1.01, "buy"),
        (0.99, 220 * 1.01, "buy"),
        (0.98, 220 * 1.02, "buy"),
    ],
)
def test_create_stoploss_order_binance(default_conf, mocker, limitratio, expected, side, trademode):
    api_mock = MagicMock()
    order_id = f"test_prod_buy_{randint(0, 10 ** 6)}"
    order_type = "stop_loss_limit" if trademode == TradingMode.SPOT else "stop"

    api_mock.create_order = MagicMock(return_value={"id": order_id, "info": {"foo": "bar"}})
    default_conf["dry_run"] = False
    default_conf["margin_mode"] = MarginMode.ISOLATED
    default_conf["trading_mode"] = trademode
    mocker.patch(f"{EXMS}.amount_to_precision", lambda s, x, y: y)
    mocker.patch(f"{EXMS}.price_to_precision", lambda s, x, y, **kwargs: y)

    exchange = get_patched_exchange(mocker, default_conf, api_mock, "binance")

    with pytest.raises(InvalidOrderException):
        order = exchange.create_stoploss(
            pair="ETH/BTC",
            amount=1,
            stop_price=190,
            side=side,
            order_types={"stoploss": "limit", "stoploss_on_exchange_limit_ratio": 1.05},
            leverage=1.0,
        )

    api_mock.create_order.reset_mock()
    order_types = {"stoploss": "limit", "stoploss_price_type": "mark"}
    if limitratio is not None:
        order_types.update({"stoploss_on_exchange_limit_ratio": limitratio})

    order = exchange.create_stoploss(
        pair="ETH/BTC", amount=1, stop_price=220, order_types=order_types, side=side, leverage=1.0
    )

    assert "id" in order
    assert "info" in order
    assert order["id"] == order_id
    assert api_mock.create_order.call_args_list[0][1]["symbol"] == "ETH/BTC"
    assert api_mock.create_order.call_args_list[0][1]["type"] == order_type
    assert api_mock.create_order.call_args_list[0][1]["side"] == side
    assert api_mock.create_order.call_args_list[0][1]["amount"] == 1
    # Price should be 1% below stopprice
    assert api_mock.create_order.call_args_list[0][1]["price"] == expected
    if trademode == TradingMode.SPOT:
        params_dict = {"stopPrice": 220}
    else:
        params_dict = {"stopPrice": 220, "reduceOnly": True, "workingType": "MARK_PRICE"}
    assert api_mock.create_order.call_args_list[0][1]["params"] == params_dict

    # test exception handling
    with pytest.raises(DependencyException):
        api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
        exchange = get_patched_exchange(mocker, default_conf, api_mock, "binance")
        exchange.create_stoploss(
            pair="ETH/BTC", amount=1, stop_price=220, order_types={}, side=side, leverage=1.0
        )

    with pytest.raises(InvalidOrderException):
        api_mock.create_order = MagicMock(
            side_effect=ccxt.InvalidOrder("binance Order would trigger immediately.")
        )
        exchange = get_patched_exchange(mocker, default_conf, api_mock, "binance")
        exchange.create_stoploss(
            pair="ETH/BTC", amount=1, stop_price=220, order_types={}, side=side, leverage=1.0
        )

    ccxt_exceptionhandlers(
        mocker,
        default_conf,
        api_mock,
        "binance",
        "create_stoploss",
        "create_order",
        retries=1,
        pair="ETH/BTC",
        amount=1,
        stop_price=220,
        order_types={},
        side=side,
        leverage=1.0,
    )


def test_create_stoploss_order_dry_run_binance(default_conf, mocker):
    api_mock = MagicMock()
    order_type = "stop_loss_limit"
    default_conf["dry_run"] = True
    mocker.patch(f"{EXMS}.amount_to_precision", lambda s, x, y: y)
    mocker.patch(f"{EXMS}.price_to_precision", lambda s, x, y, **kwargs: y)

    exchange = get_patched_exchange(mocker, default_conf, api_mock, "binance")

    with pytest.raises(InvalidOrderException):
        order = exchange.create_stoploss(
            pair="ETH/BTC",
            amount=1,
            stop_price=190,
            side="sell",
            order_types={"stoploss_on_exchange_limit_ratio": 1.05},
            leverage=1.0,
        )

    api_mock.create_order.reset_mock()

    order = exchange.create_stoploss(
        pair="ETH/BTC", amount=1, stop_price=220, order_types={}, side="sell", leverage=1.0
    )

    assert "id" in order
    assert "info" in order
    assert "type" in order

    assert order["type"] == order_type
    assert order["price"] == 220
    assert order["amount"] == 1


@pytest.mark.parametrize(
    "sl1,sl2,sl3,side", [(1501, 1499, 1501, "sell"), (1499, 1501, 1499, "buy")]
)
def test_stoploss_adjust_binance(mocker, default_conf, sl1, sl2, sl3, side):
    exchange = get_patched_exchange(mocker, default_conf, exchange="binance")
    order = {
        "type": "stop_loss_limit",
        "price": 1500,
        "stopPrice": 1500,
        "info": {"stopPrice": 1500},
    }
    assert exchange.stoploss_adjust(sl1, order, side=side)
    assert not exchange.stoploss_adjust(sl2, order, side=side)


@pytest.mark.parametrize(
    "pair, is_short, trading_mode, margin_mode, wallet_balance, "
    "maintenance_amt, amount, open_rate, open_trades,"
    "mm_ratio, expected",
    [
        (
            "ETH/USDT:USDT",
            False,
            "futures",
            "isolated",
            1535443.01,
            135365.00,
            3683.979,
            1456.84,
            [],
            0.10,
            1114.78,
        ),
        (
            "ETH/USDT:USDT",
            False,
            "futures",
            "isolated",
            1535443.01,
            16300.000,
            109.488,
            32481.980,
            [],
            0.025,
            18778.73,
        ),
        (
            "ETH/USDT:USDT",
            False,
            "futures",
            "cross",
            1535443.01,
            135365.00,
            3683.979,  # amount
            1456.84,  # open_rate
            [
                {
                    # From calc example
                    "pair": "BTC/USDT:USDT",
                    "open_rate": 32481.98,
                    "amount": 109.488,
                    "stake_amount": 3556387.02624,  # open_rate * amount
                    "mark_price": 31967.27,
                    "mm_ratio": 0.025,
                    "maintenance_amt": 16300.0,
                },
                {
                    # From calc example
                    "pair": "ETH/USDT:USDT",
                    "open_rate": 1456.84,
                    "amount": 3683.979,
                    "stake_amount": 5366967.96,
                    "mark_price": 1335.18,
                    "mm_ratio": 0.10,
                    "maintenance_amt": 135365.00,
                },
            ],
            0.10,
            1153.26,
        ),
        (
            "BTC/USDT:USDT",
            False,
            "futures",
            "cross",
            1535443.01,
            16300.0,
            109.488,  # amount
            32481.980,  # open_rate
            [
                {
                    # From calc example
                    "pair": "BTC/USDT:USDT",
                    "open_rate": 32481.98,
                    "amount": 109.488,
                    "stake_amount": 3556387.02624,  # open_rate * amount
                    "mark_price": 31967.27,
                    "mm_ratio": 0.025,
                    "maintenance_amt": 16300.0,
                },
                {
                    # From calc example
                    "pair": "ETH/USDT:USDT",
                    "open_rate": 1456.84,
                    "amount": 3683.979,
                    "stake_amount": 5366967.96,
                    "mark_price": 1335.18,
                    "mm_ratio": 0.10,
                    "maintenance_amt": 135365.00,
                },
            ],
            0.025,
            26316.89,
        ),
    ],
)
def test_liquidation_price_binance(
    mocker,
    default_conf,
    pair,
    is_short,
    trading_mode,
    margin_mode,
    wallet_balance,
    maintenance_amt,
    amount,
    open_rate,
    open_trades,
    mm_ratio,
    expected,
):
    default_conf["trading_mode"] = trading_mode
    default_conf["margin_mode"] = margin_mode
    default_conf["liquidation_buffer"] = 0.0
    mocker.patch(f"{EXMS}.price_to_precision", lambda s, x, y, **kwargs: y)
    exchange = get_patched_exchange(mocker, default_conf, exchange="binance")

    def get_maint_ratio(pair_, stake_amount):
        if pair_ != pair:
            oc = [c for c in open_trades if c["pair"] == pair_][0]
            return oc["mm_ratio"], oc["maintenance_amt"]
        return mm_ratio, maintenance_amt

    def fetch_funding_rates(*args, **kwargs):
        return {
            t["pair"]: {
                "symbol": t["pair"],
                "markPrice": t["mark_price"],
            }
            for t in open_trades
        }

    exchange.get_maintenance_ratio_and_amt = get_maint_ratio
    exchange.fetch_funding_rates = fetch_funding_rates

    open_trade_objects = [
        Trade(
            pair=t["pair"],
            open_rate=t["open_rate"],
            amount=t["amount"],
            stake_amount=t["stake_amount"],
            fee_open=0,
        )
        for t in open_trades
    ]

    assert (
        pytest.approx(
            round(
                exchange.get_liquidation_price(
                    pair=pair,
                    open_rate=open_rate,
                    is_short=is_short,
                    wallet_balance=wallet_balance,
                    amount=amount,
                    stake_amount=open_rate * amount,
                    leverage=5,
                    open_trades=open_trade_objects,
                ),
                2,
            )
        )
        == expected
    )


def test_fill_leverage_tiers_binance(default_conf, mocker):
    api_mock = MagicMock()
    api_mock.fetch_leverage_tiers = MagicMock(
        return_value={
            "ADA/BUSD": [
                {
                    "tier": 1,
                    "minNotional": 0,
                    "maxNotional": 100000,
                    "maintenanceMarginRate": 0.025,
                    "maxLeverage": 20,
                    "info": {
                        "bracket": "1",
                        "initialLeverage": "20",
                        "maxNotional": "100000",
                        "minNotional": "0",
                        "maintMarginRatio": "0.025",
                        "cum": "0.0",
                    },
                },
                {
                    "tier": 2,
                    "minNotional": 100000,
                    "maxNotional": 500000,
                    "maintenanceMarginRate": 0.05,
                    "maxLeverage": 10,
                    "info": {
                        "bracket": "2",
                        "initialLeverage": "10",
                        "maxNotional": "500000",
                        "minNotional": "100000",
                        "maintMarginRatio": "0.05",
                        "cum": "2500.0",
                    },
                },
                {
                    "tier": 3,
                    "minNotional": 500000,
                    "maxNotional": 1000000,
                    "maintenanceMarginRate": 0.1,
                    "maxLeverage": 5,
                    "info": {
                        "bracket": "3",
                        "initialLeverage": "5",
                        "maxNotional": "1000000",
                        "minNotional": "500000",
                        "maintMarginRatio": "0.1",
                        "cum": "27500.0",
                    },
                },
                {
                    "tier": 4,
                    "minNotional": 1000000,
                    "maxNotional": 2000000,
                    "maintenanceMarginRate": 0.15,
                    "maxLeverage": 3,
                    "info": {
                        "bracket": "4",
                        "initialLeverage": "3",
                        "maxNotional": "2000000",
                        "minNotional": "1000000",
                        "maintMarginRatio": "0.15",
                        "cum": "77500.0",
                    },
                },
                {
                    "tier": 5,
                    "minNotional": 2000000,
                    "maxNotional": 5000000,
                    "maintenanceMarginRate": 0.25,
                    "maxLeverage": 2,
                    "info": {
                        "bracket": "5",
                        "initialLeverage": "2",
                        "maxNotional": "5000000",
                        "minNotional": "2000000",
                        "maintMarginRatio": "0.25",
                        "cum": "277500.0",
                    },
                },
                {
                    "tier": 6,
                    "minNotional": 5000000,
                    "maxNotional": 30000000,
                    "maintenanceMarginRate": 0.5,
                    "maxLeverage": 1,
                    "info": {
                        "bracket": "6",
                        "initialLeverage": "1",
                        "maxNotional": "30000000",
                        "minNotional": "5000000",
                        "maintMarginRatio": "0.5",
                        "cum": "1527500.0",
                    },
                },
            ],
            "ZEC/USDT": [
                {
                    "tier": 1,
                    "minNotional": 0,
                    "maxNotional": 50000,
                    "maintenanceMarginRate": 0.01,
                    "maxLeverage": 50,
                    "info": {
                        "bracket": "1",
                        "initialLeverage": "50",
                        "maxNotional": "50000",
                        "minNotional": "0",
                        "maintMarginRatio": "0.01",
                        "cum": "0.0",
                    },
                },
                {
                    "tier": 2,
                    "minNotional": 50000,
                    "maxNotional": 150000,
                    "maintenanceMarginRate": 0.025,
                    "maxLeverage": 20,
                    "info": {
                        "bracket": "2",
                        "initialLeverage": "20",
                        "maxNotional": "150000",
                        "minNotional": "50000",
                        "maintMarginRatio": "0.025",
                        "cum": "750.0",
                    },
                },
                {
                    "tier": 3,
                    "minNotional": 150000,
                    "maxNotional": 250000,
                    "maintenanceMarginRate": 0.05,
                    "maxLeverage": 10,
                    "info": {
                        "bracket": "3",
                        "initialLeverage": "10",
                        "maxNotional": "250000",
                        "minNotional": "150000",
                        "maintMarginRatio": "0.05",
                        "cum": "4500.0",
                    },
                },
                {
                    "tier": 4,
                    "minNotional": 250000,
                    "maxNotional": 500000,
                    "maintenanceMarginRate": 0.1,
                    "maxLeverage": 5,
                    "info": {
                        "bracket": "4",
                        "initialLeverage": "5",
                        "maxNotional": "500000",
                        "minNotional": "250000",
                        "maintMarginRatio": "0.1",
                        "cum": "17000.0",
                    },
                },
                {
                    "tier": 5,
                    "minNotional": 500000,
                    "maxNotional": 1000000,
                    "maintenanceMarginRate": 0.125,
                    "maxLeverage": 4,
                    "info": {
                        "bracket": "5",
                        "initialLeverage": "4",
                        "maxNotional": "1000000",
                        "minNotional": "500000",
                        "maintMarginRatio": "0.125",
                        "cum": "29500.0",
                    },
                },
                {
                    "tier": 6,
                    "minNotional": 1000000,
                    "maxNotional": 2000000,
                    "maintenanceMarginRate": 0.25,
                    "maxLeverage": 2,
                    "info": {
                        "bracket": "6",
                        "initialLeverage": "2",
                        "maxNotional": "2000000",
                        "minNotional": "1000000",
                        "maintMarginRatio": "0.25",
                        "cum": "154500.0",
                    },
                },
                {
                    "tier": 7,
                    "minNotional": 2000000,
                    "maxNotional": 30000000,
                    "maintenanceMarginRate": 0.5,
                    "maxLeverage": 1,
                    "info": {
                        "bracket": "7",
                        "initialLeverage": "1",
                        "maxNotional": "30000000",
                        "minNotional": "2000000",
                        "maintMarginRatio": "0.5",
                        "cum": "654500.0",
                    },
                },
            ],
        }
    )
    default_conf["dry_run"] = False
    default_conf["trading_mode"] = TradingMode.FUTURES
    default_conf["margin_mode"] = MarginMode.ISOLATED
    exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange="binance")
    exchange.fill_leverage_tiers()

    assert exchange._leverage_tiers == {
        "ADA/BUSD": [
            {
                "minNotional": 0,
                "maxNotional": 100000,
                "maintenanceMarginRate": 0.025,
                "maxLeverage": 20,
                "maintAmt": 0.0,
            },
            {
                "minNotional": 100000,
                "maxNotional": 500000,
                "maintenanceMarginRate": 0.05,
                "maxLeverage": 10,
                "maintAmt": 2500.0,
            },
            {
                "minNotional": 500000,
                "maxNotional": 1000000,
                "maintenanceMarginRate": 0.1,
                "maxLeverage": 5,
                "maintAmt": 27500.0,
            },
            {
                "minNotional": 1000000,
                "maxNotional": 2000000,
                "maintenanceMarginRate": 0.15,
                "maxLeverage": 3,
                "maintAmt": 77500.0,
            },
            {
                "minNotional": 2000000,
                "maxNotional": 5000000,
                "maintenanceMarginRate": 0.25,
                "maxLeverage": 2,
                "maintAmt": 277500.0,
            },
            {
                "minNotional": 5000000,
                "maxNotional": 30000000,
                "maintenanceMarginRate": 0.5,
                "maxLeverage": 1,
                "maintAmt": 1527500.0,
            },
        ],
        "ZEC/USDT": [
            {
                "minNotional": 0,
                "maxNotional": 50000,
                "maintenanceMarginRate": 0.01,
                "maxLeverage": 50,
                "maintAmt": 0.0,
            },
            {
                "minNotional": 50000,
                "maxNotional": 150000,
                "maintenanceMarginRate": 0.025,
                "maxLeverage": 20,
                "maintAmt": 750.0,
            },
            {
                "minNotional": 150000,
                "maxNotional": 250000,
                "maintenanceMarginRate": 0.05,
                "maxLeverage": 10,
                "maintAmt": 4500.0,
            },
            {
                "minNotional": 250000,
                "maxNotional": 500000,
                "maintenanceMarginRate": 0.1,
                "maxLeverage": 5,
                "maintAmt": 17000.0,
            },
            {
                "minNotional": 500000,
                "maxNotional": 1000000,
                "maintenanceMarginRate": 0.125,
                "maxLeverage": 4,
                "maintAmt": 29500.0,
            },
            {
                "minNotional": 1000000,
                "maxNotional": 2000000,
                "maintenanceMarginRate": 0.25,
                "maxLeverage": 2,
                "maintAmt": 154500.0,
            },
            {
                "minNotional": 2000000,
                "maxNotional": 30000000,
                "maintenanceMarginRate": 0.5,
                "maxLeverage": 1,
                "maintAmt": 654500.0,
            },
        ],
    }

    api_mock = MagicMock()
    api_mock.load_leverage_tiers = MagicMock()
    type(api_mock).has = PropertyMock(return_value={"fetchLeverageTiers": True})

    ccxt_exceptionhandlers(
        mocker,
        default_conf,
        api_mock,
        "binance",
        "fill_leverage_tiers",
        "fetch_leverage_tiers",
    )


def test_fill_leverage_tiers_binance_dryrun(default_conf, mocker, leverage_tiers):
    api_mock = MagicMock()
    default_conf["trading_mode"] = TradingMode.FUTURES
    default_conf["margin_mode"] = MarginMode.ISOLATED
    exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange="binance")
    exchange.fill_leverage_tiers()
    assert len(exchange._leverage_tiers.keys()) > 100
    for key, value in leverage_tiers.items():
        v = exchange._leverage_tiers[key]
        assert isinstance(v, list)
        # Assert if conftest leverage tiers have less or equal tiers than the exchange
        assert len(v) >= len(value)


def test_additional_exchange_init_binance(default_conf, mocker):
    api_mock = MagicMock()
    api_mock.fapiPrivateGetPositionSideDual = MagicMock(return_value={"dualSidePosition": True})
    api_mock.fapiPrivateGetMultiAssetsMargin = MagicMock(return_value={"multiAssetsMargin": True})
    default_conf["dry_run"] = False
    default_conf["trading_mode"] = TradingMode.FUTURES
    default_conf["margin_mode"] = MarginMode.ISOLATED
    with pytest.raises(
        OperationalException,
        match=r"Hedge Mode is not supported.*\nMulti-Asset Mode is not supported.*",
    ):
        get_patched_exchange(mocker, default_conf, exchange="binance", api_mock=api_mock)
    api_mock.fapiPrivateGetPositionSideDual = MagicMock(return_value={"dualSidePosition": False})
    api_mock.fapiPrivateGetMultiAssetsMargin = MagicMock(return_value={"multiAssetsMargin": False})
    exchange = get_patched_exchange(mocker, default_conf, exchange="binance", api_mock=api_mock)
    assert exchange
    ccxt_exceptionhandlers(
        mocker,
        default_conf,
        api_mock,
        "binance",
        "additional_exchange_init",
        "fapiPrivateGetPositionSideDual",
    )


def test__set_leverage_binance(mocker, default_conf):
    api_mock = MagicMock()
    api_mock.set_leverage = MagicMock()
    type(api_mock).has = PropertyMock(return_value={"setLeverage": True})
    default_conf["dry_run"] = False
    default_conf["trading_mode"] = TradingMode.FUTURES
    default_conf["margin_mode"] = MarginMode.ISOLATED

    exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange="binance")
    exchange._set_leverage(3.2, "BTC/USDT:USDT")
    assert api_mock.set_leverage.call_count == 1
    # Leverage is rounded to 3.
    assert api_mock.set_leverage.call_args_list[0][1]["leverage"] == 3
    assert api_mock.set_leverage.call_args_list[0][1]["symbol"] == "BTC/USDT:USDT"

    ccxt_exceptionhandlers(
        mocker,
        default_conf,
        api_mock,
        "binance",
        "_set_leverage",
        "set_leverage",
        pair="XRP/USDT",
        leverage=5.0,
    )


def patch_binance_vision_ohlcv(mocker, start, archive_end, api_end, timeframe):
    def make_storage(start: datetime, end: datetime, timeframe: str):
        date = pd.date_range(start, end, freq=timeframe.replace("m", "min"))
        df = pd.DataFrame(
            data=dict(date=date, open=1.0, high=1.0, low=1.0, close=1.0),
        )
        return df

    archive_storage = make_storage(start, archive_end, timeframe)
    api_storage = make_storage(start, api_end, timeframe)

    ohlcv = [[dt_ts(start), 1, 1, 1, 1]]
    # (pair, timeframe, candle_type, ohlcv, True)
    candle_history = [None, None, None, ohlcv, None]

    def get_historic_ohlcv(
        # self,
        pair: str,
        timeframe: str,
        since_ms: int,
        candle_type: CandleType,
        is_new_pair: bool = False,
        until_ms: int | None = None,
    ):
        since = dt_from_ts(since_ms)
        until = dt_from_ts(until_ms) if until_ms else api_end + timedelta(seconds=1)
        return api_storage.loc[(api_storage["date"] >= since) & (api_storage["date"] < until)]

    async def download_archive_ohlcv(
        candle_type,
        pair,
        timeframe,
        since_ms,
        until_ms,
        markets=None,
        stop_on_404=False,
    ):
        since = dt_from_ts(since_ms)
        until = dt_from_ts(until_ms) if until_ms else archive_end + timedelta(seconds=1)
        if since < start:
            pass
        return archive_storage.loc[
            (archive_storage["date"] >= since) & (archive_storage["date"] < until)
        ]

    candle_mock = mocker.patch(f"{EXMS}._async_get_candle_history", return_value=candle_history)
    api_mock = mocker.patch(f"{EXMS}.get_historic_ohlcv", side_effect=get_historic_ohlcv)
    archive_mock = mocker.patch(
        "freqtrade.exchange.binance.download_archive_ohlcv", side_effect=download_archive_ohlcv
    )
    return candle_mock, api_mock, archive_mock


@pytest.mark.parametrize(
    "timeframe,is_new_pair,since,until,first_date,last_date,candle_called,archive_called,"
    "api_called",
    [
        (
            "1m",
            True,
            dt_utc(2020, 1, 1),
            dt_utc(2020, 1, 2),
            dt_utc(2020, 1, 1),
            dt_utc(2020, 1, 1, 23, 59),
            True,
            True,
            False,
        ),
        (
            "1m",
            True,
            dt_utc(2020, 1, 1),
            dt_utc(2020, 1, 3),
            dt_utc(2020, 1, 1),
            dt_utc(2020, 1, 2, 23, 59),
            True,
            True,
            True,
        ),
        (
            "1m",
            True,
            dt_utc(2020, 1, 2),
            dt_utc(2020, 1, 2, 1),
            dt_utc(2020, 1, 2),
            dt_utc(2020, 1, 2, 0, 59),
            True,
            False,
            True,
        ),
        (
            "1m",
            False,
            dt_utc(2020, 1, 1),
            dt_utc(2020, 1, 2),
            dt_utc(2020, 1, 1),
            dt_utc(2020, 1, 1, 23, 59),
            False,
            True,
            False,
        ),
        (
            "1m",
            True,
            dt_utc(2019, 1, 1),
            dt_utc(2020, 1, 2),
            dt_utc(2020, 1, 1),
            dt_utc(2020, 1, 1, 23, 59),
            True,
            True,
            False,
        ),
        (
            "1m",
            False,
            dt_utc(2019, 1, 1),
            dt_utc(2020, 1, 2),
            dt_utc(2020, 1, 1),
            dt_utc(2020, 1, 1, 23, 59),
            False,
            True,
            False,
        ),
        (
            "1m",
            False,
            dt_utc(2019, 1, 1),
            dt_utc(2019, 1, 2),
            None,
            None,
            False,
            True,
            True,
        ),
        (
            "1m",
            True,
            dt_utc(2019, 1, 1),
            dt_utc(2019, 1, 2),
            None,
            None,
            True,
            False,
            False,
        ),
        (
            "1m",
            False,
            dt_utc(2021, 1, 1),
            dt_utc(2021, 1, 2),
            None,
            None,
            False,
            False,
            False,
        ),
        (
            "1m",
            True,
            dt_utc(2021, 1, 1),
            dt_utc(2021, 1, 2),
            None,
            None,
            True,
            False,
            False,
        ),
        (
            "1h",
            False,
            dt_utc(2020, 1, 1),
            dt_utc(2020, 1, 2),
            dt_utc(2020, 1, 1),
            dt_utc(2020, 1, 1, 23),
            False,
            False,
            True,
        ),
        (
            "1m",
            False,
            dt_utc(2020, 1, 1),
            dt_utc(2020, 1, 1, 3, 50, 30),
            dt_utc(2020, 1, 1),
            dt_utc(2020, 1, 1, 3, 50),
            False,
            True,
            False,
        ),
    ],
)
def test_get_historic_ohlcv_binance(
    mocker,
    default_conf,
    timeframe,
    is_new_pair,
    since,
    until,
    first_date,
    last_date,
    candle_called,
    archive_called,
    api_called,
):
    exchange = get_patched_exchange(mocker, default_conf, exchange="binance")

    start = dt_utc(2020, 1, 1)
    archive_end = dt_utc(2020, 1, 2)
    api_end = dt_utc(2020, 1, 3)
    candle_mock, api_mock, archive_mock = patch_binance_vision_ohlcv(
        mocker, start=start, archive_end=archive_end, api_end=api_end, timeframe=timeframe
    )

    candle_type = CandleType.SPOT
    pair = "BTC/USDT"

    since_ms = dt_ts(since)
    until_ms = dt_ts(until)

    df = exchange.get_historic_ohlcv(pair, timeframe, since_ms, candle_type, is_new_pair, until_ms)

    if df.empty:
        assert first_date is None
        assert last_date is None
    else:
        assert df["date"].iloc[0] == first_date
        assert df["date"].iloc[-1] == last_date
        assert (
            df["date"].diff().iloc[1:] == timedelta(seconds=timeframe_to_seconds(timeframe))
        ).all()

    if candle_called:
        candle_mock.assert_called_once()
    if archive_called:
        archive_mock.assert_called_once()
    if api_called:
        api_mock.assert_called_once()


@pytest.mark.parametrize(
    "pair,notional_value,mm_ratio,amt",
    [
        ("XRP/USDT:USDT", 0.0, 0.025, 0),
        ("BNB/USDT:USDT", 100.0, 0.0065, 0),
        ("BTC/USDT:USDT", 170.30, 0.004, 0),
        ("XRP/USDT:USDT", 999999.9, 0.1, 27500.0),
        ("BNB/USDT:USDT", 5000000.0, 0.15, 233035.0),
        ("BTC/USDT:USDT", 600000000, 0.5, 1.997038e8),
    ],
)
def test_get_maintenance_ratio_and_amt_binance(
    default_conf,
    mocker,
    leverage_tiers,
    pair,
    notional_value,
    mm_ratio,
    amt,
):
    mocker.patch(f"{EXMS}.exchange_has", return_value=True)
    exchange = get_patched_exchange(mocker, default_conf, exchange="binance")
    exchange._leverage_tiers = leverage_tiers
    (result_ratio, result_amt) = exchange.get_maintenance_ratio_and_amt(pair, notional_value)
    assert (round(result_ratio, 8), round(result_amt, 8)) == (mm_ratio, amt)


async def test__async_get_trade_history_id_binance(default_conf_usdt, mocker, fetch_trades_result):
    exchange = get_patched_exchange(mocker, default_conf_usdt, exchange="binance")

    async def mock_get_trade_hist(pair, *args, **kwargs):
        if "since" in kwargs:
            # older than initial call
            if kwargs["since"] < 1565798399752:
                return []
            else:
                # Don't expect to get here
                raise ValueError("Unexpected call")
                # return fetch_trades_result[:-2]
        elif kwargs.get("params", {}).get(exchange._trades_pagination_arg) == "0":
            # Return first 3
            return fetch_trades_result[:-2]
        elif kwargs.get("params", {}).get(exchange._trades_pagination_arg) in (
            fetch_trades_result[-3]["id"],
            1565798399752,
        ):
            # Return 2
            return fetch_trades_result[-3:-1]
        else:
            # Return last 2
            return fetch_trades_result[-2:]

    exchange._api_async.fetch_trades = MagicMock(side_effect=mock_get_trade_hist)

    pair = "ETH/BTC"
    ret = await exchange._async_get_trade_history_id(
        pair,
        since=fetch_trades_result[0]["timestamp"],
        until=fetch_trades_result[-1]["timestamp"] - 1,
    )
    assert ret[0] == pair
    assert isinstance(ret[1], list)
    assert exchange._api_async.fetch_trades.call_count == 4

    fetch_trades_cal = exchange._api_async.fetch_trades.call_args_list
    # first call (using since, not fromId)
    assert fetch_trades_cal[0][0][0] == pair
    assert fetch_trades_cal[0][1]["since"] == fetch_trades_result[0]["timestamp"]

    # 2nd call
    assert fetch_trades_cal[1][0][0] == pair
    assert "params" in fetch_trades_cal[1][1]
    pagination_arg = exchange._ft_has["trades_pagination_arg"]
    assert pagination_arg in fetch_trades_cal[1][1]["params"]
    # Initial call was with from_id = "0"
    assert fetch_trades_cal[1][1]["params"][pagination_arg] == "0"

    assert fetch_trades_cal[2][1]["params"][pagination_arg] != "0"
    assert fetch_trades_cal[3][1]["params"][pagination_arg] != "0"

    # Clean up event loop to avoid warnings
    exchange.close()
